﻿using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Adaptive.Agrona;
using Adaptive.Agrona.Concurrent;
using Microsoft.Extensions.Logging;
using Unity.Commons.Client;
using Unity.Commons.Client.Statistics;
using Unity.Commons;
using Unity.Commons.Client.Buffer;
using Unity.Commons.Client.Generator;
using Unity.Commons.Client.Threading;
using Unity.Connections;
using Unity.Credentials;
using Unity.Logger;
using Unity.Type;

namespace Unity.Debug
{
    public class ConsoleUnityClient : IUnityClient, IUnityStatistics
    {
        private static readonly ILogger Logger = 
            UnityLogger.ConsoleLoggerFactory.CreateLogger(nameof(ConsoleUnityClient));

        public static readonly string Version = "0.7.3.2-debug";

        private static readonly int ResourceThreadSleepingPeriod = 500_000_000;
        private static readonly int PreAllocatedLength = 1024 * 1024 * 8;
        
        private UnityGenerator _generator;
        private readonly UnityGenerator _initGenerator;
        private readonly string _csiId;
        private readonly IUnityCredentials _credentials;

        private readonly IUnityBuffer _mainRingBuffer;
        private readonly IUnityBuffer _offlineRingBuffer;

        private readonly AgentRunner _agentRunner;
        private readonly IUnityResourceThread _resourceThread;

        private static AtomicLong Generated = new AtomicLong();
        private static AtomicLong Dropped = new AtomicLong();

        private volatile bool _isBlocking = false;

        private readonly IAtomicBuffer _preAllocatedBuffer;
        private readonly int _maximumPreAllocatedSize;
        private int _preAllocatedPosition;

        private EntityType _lastPreGeneratedEntityType = EntityType.Unknown;

        private ErrorHandler _errorHandler = (Exception exception) => { };

        public ConsoleUnityClient(string csiId, IUnityCredentials credentials, IUnityBuffer mainBuffer, IUnityBuffer offlineBuffer, string bufferFile,
            IUnityProducer resourceProducer)
            : this(csiId, credentials, mainBuffer, offlineBuffer, bufferFile, resourceProducer, 0)
        {
        }

        public ConsoleUnityClient(string csiId, IUnityCredentials credentials, IUnityBuffer mainBuffer, IUnityBuffer offlineBuffer,
            string bufferFile, IUnityProducer resourceProducer, int initialCounter)
        {
            _csiId = csiId;
            _credentials = credentials;
            _generator = new UnityGenerator(_csiId);
            _initGenerator = _generator;
            if (initialCounter > 0)
            {
                _generator.SetCounter(initialCounter);
            }

            _mainRingBuffer = mainBuffer;
            _offlineRingBuffer = offlineBuffer;
            _mainRingBuffer.Start();
            _resourceThread = new UnityResourceThread(this, resourceProducer, csiId);
            _agentRunner = new AgentRunner(new SleepingIdleStrategy(ResourceThreadSleepingPeriod),
                _errorHandler, null, _resourceThread);
            AgentRunner.StartOnThread(_agentRunner);
            _preAllocatedBuffer = new UnsafeBuffer(new ByteBuffer(PreAllocatedLength, 0));
            _maximumPreAllocatedSize = PreAllocatedLength / UnityGenerator.MessageLength;
        }

        private long GetVersion()
        {
            return UnityTimestamp.GetNanoTime();
        }
        
        public bool Connect(int timeout)
        {
            Logger.LogTrace("Call method Connect");
            Task<bool> connectionTask = AsyncConnect();
            if (connectionTask.Wait(timeout))
            {
                return connectionTask.Result;
            }
            return false;
        }

        private Task<bool> AsyncConnect()
        {
            return _resourceThread.RunAsync(InternalConnect);
        }

        private bool InternalConnect()
        {
            _resourceThread.GetProducer().Connect(_credentials);
            return _mainRingBuffer.GetProducer().Connect(_credentials);
        }
        
        public void Close()
        {
            Logger.LogTrace("Call method Close");
            _resourceThread?.GetProducer().Close();
            _mainRingBuffer?.GetProducer().Close();
            _resourceThread?.Close();
            _agentRunner?.Dispose();
        }

        public void Dispose()
        {
            Logger.LogTrace("Call method Dispose");
            Close();
        }

        public int Generate(EntityType entityType, IMutableDirectBuffer buffer, int offset)
        {
            Logger.LogTrace("Call method Generate");

            long version = GetVersion();
            int mainClaim = _mainRingBuffer.Claim(UnityGenerator.MessageLength);
            int idLength = 0;
            if (mainClaim > 0)
            {
                idLength = generateIdViaRingBuffer(entityType, version, _mainRingBuffer, mainClaim, buffer, offset);
                Generated.IncrementAndGet();
            }
            else
            {
                Dropped.IncrementAndGet();
            }

            buffer.PutLong(offset + idLength, version);
            return idLength;
        }
        
        public long Update(EntityType entityType, IMutableDirectBuffer buffer, int offset, int length)
        {
            Logger.LogTrace("Call method Update");
            long version = GetVersion();
            int mainClaim = _mainRingBuffer.Claim(UnityGenerator.MessageLength);
            bool updated = false;
            if (mainClaim > 0)
            {
                updated = updateIdViaRingBuffer(buffer, offset, length, entityType, version, _mainRingBuffer,
                    mainClaim);
            }
            else
            {
                // Fallback
            }

            if (updated)
            {
                Generated.IncrementAndGet();
            }
            else
            {
                Dropped.IncrementAndGet();
            }
            return version;
        }

        public ClientMode GetMode()
        {
            Logger.LogTrace("Call method GetMode");
            return ClientMode.Offline;
        }

        public IUnityStatistics GetStatistics()
        {
            Logger.LogTrace("Call method GetStatistics");
            return null;
        }

        private int generateIdViaRingBuffer(EntityType entityType, long version, IUnityBuffer ringBuffer, int claim,
            IMutableDirectBuffer buffer, int offset)
        {
            int idLength =
                _generator.GenerateMetadataAndCopy(entityType, version, ringBuffer.GetBuffer(), claim, buffer, offset);
            if (idLength == (int)UnityGenerator.CounterStatus.Overflow)
            {
                RotateGenerator();
                idLength = _generator.GenerateMetadataAndCopy(entityType, version, ringBuffer.GetBuffer(), claim, buffer, offset);
            }

            ringBuffer.Commit(claim);
            return idLength;
        }

        private bool updateIdViaRingBuffer(IMutableDirectBuffer idBuffer, int idOffset, int idLength,
            EntityType entityType, long version, IUnityBuffer ringBuffer, int claim)
        {
            try
            {
                _generator.UpdateMetadata(idBuffer, idOffset, idLength, entityType, version, ringBuffer.GetBuffer(), claim);
                ringBuffer.Commit(claim);
                return true;
            } 
            catch(Exception exception)
            {
                Logger.LogError("updateViaRingBuffer {}", exception);
                return false;
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        private void RotateGenerator()
        {
            _generator = _resourceThread.GetNewGenerator();
        }

        public long GetPublishedStream()
        {
            return _mainRingBuffer.GetPublished();
        }

        public long GetPublishedOffline()
        {
            return _offlineRingBuffer == null ? 0 : _offlineRingBuffer.GetPublished();
        }

        public long GetGenerated()
        {
            return Generated.Get();
        }

        public long GetDropped()
        {
            return Dropped.Get();
        }

        public long GetBufferCapacity()
        {
            return _mainRingBuffer.GetBufferCapacity();
        }

        public long GetBufferSize()
        {
            return _mainRingBuffer.GetBufferSize();
        }

        public long GetOfflineBufferCapacity()
        {
            return _offlineRingBuffer == null ? 0 : _offlineRingBuffer.GetBufferCapacity();
        }

        public long GetOfflineBufferSize()
        {
            return _offlineRingBuffer == null ? 0 : _offlineRingBuffer.GetBufferSize();
        }
    }
}
